---
title: Testing
description: Some tips on testing components that use `nuqs`
---

Since nuqs 2, you can unit-test components that use `useQueryState(s){:ts}` hooks
without needing to mock anything, by using a dedicated testing adapter that will
facilitate **setting up** your tests (with initial search params) and **asserting**
on URL changes when **acting** on your components.

## Testing hooks with React Testing Library

When testing hooks that rely on nuqs' `useQueryState(s){:ts}` with React Testing Library's
[`renderHook{:ts}`](https://testing-library.com/docs/react-testing-library/api/#renderhook) function,
you can use `withNuqsTestingAdapter{:ts}` to get a wrapper component to pass to the
`renderHook{:ts}` call:

```tsx
import { withNuqsTestingAdapter } from 'nuqs/adapters/testing'

const { result } = renderHook(() => useTheHookToTest(), {
  wrapper: withNuqsTestingAdapter({
    searchParams: { count: "42" },
  }),
})
```

<FeatureSupportMatrix introducedInVersion='2.2.0' hideFrameworks />

## Testing components with Vitest

Here is an example for Vitest and Testing Library to test a button rendering
a counter:

<Tabs items={['Vitest v1', 'Vitest v2']}>

```tsx title="counter-button.test.tsx" tab="Vitest v1"
// [!code word:withNuqsTestingAdapter]
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { withNuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
  const user = userEvent.setup()
  const onUrlUpdate = vi.fn<[UrlUpdateEvent]>()
  render(<CounterButton />, {
    // 1. Setup the test by passing initial search params / querystring:
    wrapper: withNuqsTestingAdapter({ searchParams: '?count=42', onUrlUpdate })
  })
  // 2. Act
  const button = screen.getByRole('button')
  await user.click(button)
  // 3. Assert changes in the state and in the (mocked) URL
  expect(button).toHaveTextContent('count is 43')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  const event = onUrlUpdate.mock.calls[0]![0]!
  expect(event.queryString).toBe('?count=43')
  expect(event.searchParams.get('count')).toBe('43')
  expect(event.options.history).toBe('push')
})
```

```tsx title="counter-button.test.tsx" tab="Vitest v2"
// [!code word:withNuqsTestingAdapter]
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { withNuqsTestingAdapter, type OnUrlUpdateFunction } from 'nuqs/adapters/testing'
import { describe, expect, it, vi } from 'vitest'
import { CounterButton } from './counter-button'

it('should increment the count when clicked', async () => {
  const user = userEvent.setup()
  const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
  render(<CounterButton />, {
    // 1. Setup the test by passing initial search params / querystring:
    wrapper: withNuqsTestingAdapter({ searchParams: '?count=42', onUrlUpdate })
  })
  // 2. Act
  const button = screen.getByRole('button')
  await user.click(button)
  // 3. Assert changes in the state and in the (mocked) URL
  expect(button).toHaveTextContent('count is 43')
  expect(onUrlUpdate).toHaveBeenCalledOnce()
  const event = onUrlUpdate.mock.calls[0]![0]!
  expect(event.queryString).toBe('?count=43')
  expect(event.searchParams.get('count')).toBe('43')
  expect(event.options.history).toBe('push')
})
```

</Tabs>

See issue [#259](https://github.com/47ng/nuqs/issues/259) for more testing-related discussions.

## Jest and ESM

Since nuqs 2 is an [ESM-only package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c),
there are a few hoops you need to jump through to make it work with Jest.
This is extracted from the [Jest ESM guide](https://jestjs.io/docs/ecmascript-modules).

1. Add the following options to your jest.config.ts file:

```ts title="jest.config.ts"
const config: Config = {
  // <Other options here>
  // [!code highlight:2]
  extensionsToTreatAsEsm: [".ts", ".tsx"],
  transform: {}
};
```

2. Change your test command to include the `--experimental-vm-modules` flag:

```json title="package.json"
// [!code word:--experimental-vm-modules]
{
  "scripts": {
    "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest"
  }
}
```

<Callout>
Adapt accordingly for Windows with [`cross-env`](https://www.npmjs.com/package/cross-env).
</Callout>

## API

`withNuqsTestingAdapter{:ts}` accepts the following arguments:

- `searchParams{:ts}`: The initial search params to use for the test. These can be a
  query string, a `URLSearchParams` object or a record object with string values.

```tsx
withNuqsTestingAdapter({
  searchParams: '?q=hello&limit=10'
})

withNuqsTestingAdapter({
  searchParams: new URLSearchParams('?q=hello&limit=10')
})

withNuqsTestingAdapter({
  searchParams: {
    q: 'hello',
    limit: '10' // Values are serialized strings
  }
})
```

- `onUrlUpdate{:ts}`: a function that will be called when the URL is updated
  by the component. It receives an object with:
  - the new search params as an instance of `URLSearchParams{:ts}`
  - the new rendered query string (for convenience)
  - the options used to update the URL.

- `hasMemory{:ts}`: by default, the testing adapter is **immutable**,
meaning it will always use the initial search params as a base for URL
updates. This encourages testing units of behaviour in a single test.

To make it behave like framework adapters (which do store the
updates in the URL), set `hasMemory: true{:ts}`, so subsequent updates
build up on the previous state. This memory is per-adapter instance,
and so is isolated between tests, but shared for components under the same
adapter.

<details>
<summary className='cursor-pointer'>🧪 Internal/advanced options</summary>

- `rateLimitFactor{:ts}`. By default, rate limiting is disabled when testing,
as it can lead to unexpected behaviours. Setting this to 1 will enable rate
limiting with the same factor as in production.

- `resetUrlUpdateQueueOnMount{:ts}`: clear the URL update queue before running the test.
This is `true{:ts}` by default to isolate tests, but you can set it to `false{:ts}` to keep the
URL update queue between renders and match the production behaviour more closely.

</details>


## NuqsTestingAdapter

The `withNuqsTestingAdapter{:ts}` function is a wrapper component factory function
wraps children with a `NuqsTestingAdapter{:ts}`, but you can also use
it directly:

```tsx
// [!code word:NuqsTestingAdapter]
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'

<NuqsTestingAdapter>
  <ComponentsUsingNuqs/>
</NuqsTestingAdapter>
```

It takes the same props as the arguments you can pass to `withNuqsTestingAdapter{:ts}`.

## Testing custom parsers

If you create custom parsers with `createParser{:ts}`, you will likely want to
test them.

Parsers should:
1. Define pure functions for `parse{:ts}`, `serialize{:ts}`, and `eq{:ts}`.
2. Be bijective: `parse(serialize(x)) === x{:ts}` and `serialize(parse(x)) === x{:ts}`.

To help test bijectivity, you can use helpers defined in `nuqs/testing`:

```ts
// [!code word:isParserBijective]
import {
  isParserBijective,
  testParseThenSerialize,
  testSerializeThenParse
} from 'nuqs/testing'

it('is bijective', () => {
  // Passing tests return true
  expect(isParserBijective(parseAsInteger, '42', 42)).toBe(true)
  // Failing test throws an error
  expect(() => isParserBijective(parseAsInteger, '42', 47)).toThrowError()

  // You can also test either side separately:
  expect(testParseThenSerialize(parseAsInteger, '42')).toBe(true)
  expect(testSerializeThenParse(parseAsInteger, 42)).toBe(true)
  // Those will also throw an error if the test fails,
  // which makes it easier to isolate which side failed:
  expect(() => testParseThenSerialize(parseAsInteger, 'not a number')).toThrowError()
  expect(() => testSerializeThenParse(parseAsInteger, NaN)).toThrowError()
})
```

<FeatureSupportMatrix introducedInVersion='2.4.0' hideFrameworks />
